/**
 * Copyright 2009 Dan Hansen <hansen.danr@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.googlecode.php.mojo.phar;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.archiver.MavenArchiver;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;

import com.googlecode.php.mojo.archiver.PharArchiver;

/**
 * Base class for creating a phar from project classes.
 * 
 * @author dhansen
 */
public abstract class AbstractPharMojo
	extends AbstractMojo
{

	private static final String DS = File.separator;

	private static final String TMP_STUB_FILE = "phar-stub.php";

	private static final String[] DEFAULT_EXCLUDES = new String[] { "**/package.html" };

	private static final String[] DEFAULT_INCLUDES = new String[] { "**/**" };

	/**
	 * List of files to include. Specified as fileset patterns.
	 * 
	 * @parameter
	 */
	private String[] includes;

	/**
	 * List of files to exclude. Specified as fileset patterns.
	 * 
	 * @parameter
	 */
	private String[] excludes;

	/**
	 * Directory containing the generated PHAR.
	 * 
	 * @parameter expression="${project.build.directory}"
	 * @required
	 */
	private File outputDirectory;

	/**
	 * Name of the generated PHAR.
	 * 
	 * @parameter alias="pharName" expression="${phar.finalName}"
	 *            default-value="${project.build.finalName}"
	 * @required
	 */
	private String finalName;

	/**
	 * The file extention used for the generated PHAR.
	 * 
	 * @parameter expression="${phar.fileExtension}" default-value=".phar"
	 */
	private static String fileExtension;

	/**
	 * The Phar archiver.
	 * 
	 * @required
	 */
	private PharArchiver pharArchiver;

	/**
	 * The Maven project.
	 * 
	 * @parameter expression="${project}"
	 * @required
	 * @readonly
	 */
	private MavenProject project;

	/**
	 * The archive configuration to use. See <a
	 * href="http://maven.apache.org/shared/maven-archiver/index.html">the
	 * documentation for Maven Archiver</a>.
	 * 
	 * @parameter
	 */
	final private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();

	/**
	 * Path to the default MANIFEST file to use. It will be used if
	 * <code>useDefaultManifestFile</code> is set to <code>true</code>.
	 * 
	 * @parameter 
	 *            expression="${project.build.outputDirectory}/META-INF/MANIFEST.MF"
	 * @required
	 * @readonly
	 * @since 2.2
	 */
	private File defaultManifestFile;

	/**
	 * Set this to <code>true</code> to enable the use of the
	 * <code>defaultManifestFile</code>.
	 * 
	 * @parameter expression="${phar.useDefaultManifestFile}"
	 *            default-value="false"
	 * @since 2.2
	 */
	private boolean useDefaultManifestFile;

	/**
	 * @component
	 */
	private MavenProjectHelper projectHelper;

	/**
	 * Whether creating the archive should be forced.
	 * 
	 * @parameter expression="${phar.forceCreation}" default-value="false"
	 */
	private boolean forceCreation;

	/**
	 * @parameter expression="${phar.stubFile}" default-value=""
	 */
	private File stubFile;

	/**
	 * Return the specific output directory to serve as the root for the
	 * archive.
	 * 
	 * @return File
	 */
	protected abstract File getClassesDirectory();

	/**
	 * Overload this to produce a phar with another classifier, for example a
	 * test-phar.
	 * 
	 * @return String
	 */
	protected abstract String getClassifier();

	/**
	 * Overload this to produce a test-phar, for example.
	 * 
	 * @return String
	 */
	protected abstract String getType();

	/**
	 * Generates the PHAR.
	 * 
	 * @throws MojoExecutionException
	 * @todo Add license files in META-INF directory.
	 */
	public void execute() throws MojoExecutionException
	{
		File pharFile = createArchive();

		String classifier = getClassifier();
		if ( classifier == null )
		{
			getProject().getArtifact().setFile( pharFile );
		}
		else
		{
			projectHelper.attachArtifact( getProject(), getType(), classifier, pharFile );
		}
	}

	/**
	 * Generates the PHAR.
	 * 
	 * @todo Add license files in META-INF directory.
	 * @throws MojoExecutionException
	 * @return File The archive file created.
	 */
	public File createArchive() throws MojoExecutionException
	{
		File pharFile = getPharFile( outputDirectory, finalName, getClassifier() );

		MavenArchiver archiver = new MavenArchiver();
		archiver.setArchiver( getArchiver() );
		archiver.setOutputFile( pharFile );
		archive.setForced( forceCreation );

		try
		{
			File contentDirectory = getClassesDirectory();
			if ( contentDirectory.exists() )
			{
				archiver.getArchiver().addDirectory( contentDirectory, getIncludes(), getExcludes() );
			}
			else
			{
				getLog().warn( "PHAR will be empty - no content was marked for inclusion!" );
			}

			File existingManifest = getDefaultManifestFile();

			if ( useDefaultManifestFile && existingManifest.exists() && archive.getManifestFile() == null )
			{
				getLog().info( "Adding existing MANIFEST to archive. Found under: " + existingManifest.getPath() );
				archive.setManifestFile( existingManifest );
			}

			archiver.createArchive( project, archive );

			return pharFile;
		}
		catch ( Exception e )
		{
			// TODO: improve error handling
			throw new MojoExecutionException( "Error assembling PHAR", e );
		}
	}

	/**
	 * Retrieves the stub file to be used. This will be a stub from the
	 * {@link #pharArchiver} unless an external stub is supplied via
	 * configuration.
	 * 
	 * @return File Phar stub file.
	 * @throws IOException
	 */
	protected File getPharStub() throws IOException
	{
		File pharStub = stubFile;

		if ( pharStub == null )
		{
			pharStub = createDefaultStub();
		}
		else if ( !pharStub.exists() )
		{
			getLog().warn( "Cannot load stub file: " + pharStub.toString() + " - using default stub instead!" );

			pharStub = createDefaultStub();
		}

		return pharStub;
	}

	private File createDefaultStub() throws IOException
	{
		File pharStub;
		File tmpStub = new File( outputDirectory + DS + "archive-tmp" + DS + TMP_STUB_FILE );
		File dir = tmpStub.getParentFile();

		if ( !dir.exists() && !dir.mkdirs() )
		{
			//TODO What's the appropriate exception to throw here?
			throw new IOException( "Cannot create temp archive directory: " + dir.toString() );
		}

		InputStream is = PharArchiver.getDefaultStub();
		FileOutputStream os = new FileOutputStream( tmpStub );

		byte[] buf = new byte[ 1024 ];
		int len;
		while ( ( len = is.read( buf ) ) > 0 )
		{
			os.write( buf, 0, len );
		}

		is.close();
		os.close();

		pharStub = tmpStub;
		return pharStub;
	}

	/**
	 * @param basedir
	 * @param finalName
	 * @param classifier
	 * @return File
	 */
	protected File getPharFile( File basedir, String finalName, String classifier )
	{
		String myClassifier;

		if ( classifier == null )
		{
			myClassifier = "";
		}
		else if ( classifier.trim().length() > 0 && !classifier.startsWith( "-" ) )
		{
			myClassifier = "-" + classifier;
		}
		else
		{
			myClassifier = classifier;
		}

		return new File( basedir, finalName + myClassifier + fileExtension );
	}

	/**
	 * Returns the PharArchiver.
	 * 
	 * @return PharArchiver
	 * @throws MojoExecutionException
	 */
	protected final PharArchiver getArchiver() throws MojoExecutionException
	{
		if ( pharArchiver == null )
		{
			try
			{
				pharArchiver = new PharArchiver( getPharStub() );
			}
			catch ( IOException e )
			{
				throw new MojoExecutionException( "Failed to load the stub file.", e );
			}
		}

		return pharArchiver;
	}

	/**
	 * TODO Method description for <code>getProject()</code>
	 * 
	 * @return MavenProject
	 */
	protected final MavenProject getProject()
	{
		return project;
	}

	/**
	 * Default Manifest location. Can point to a non existing file. Cannot
	 * return null.
	 * 
	 * @return File
	 */
	protected File getDefaultManifestFile()
	{
		return defaultManifestFile;
	}

	private String[] getIncludes()
	{
		String[] value;

		if ( includes != null && includes.length > 0 )
		{
			value = includes;
		}
		else
		{
			value = DEFAULT_INCLUDES;
		}

		String[] returnValue = Arrays.copyOf( value, value.length );

		return returnValue;
	}

	private String[] getExcludes()
	{
		String[] value;

		if ( excludes != null && excludes.length > 0 )
		{
			value = excludes;
		}
		else
		{
			value = DEFAULT_EXCLUDES;
		}

		String[] returnValue = Arrays.copyOf( value, value.length );

		return returnValue;
	}

}
